Skip to content

Wire auth and locale to shared.json, AI settings to cli.json#2821

Open
bcotrim wants to merge 12 commits intostu-1350-decoupled-config-devfrom
stu-1350-decoupled-config-dev-v2
Open

Wire auth and locale to shared.json, AI settings to cli.json#2821
bcotrim wants to merge 12 commits intostu-1350-decoupled-config-devfrom
stu-1350-decoupled-config-dev-v2

Conversation

@bcotrim
Copy link
Contributor

@bcotrim bcotrim commented Mar 16, 2026

Related issues

How AI was used in this PR

All code was generated via Claude Code (Opus + Haiku). Changes were reviewed and validated by the author through iterative conversation — verifying architecture decisions, confirming read/write boundaries, and running the full test suite at each step.

Proposed Changes

  • Create tools/common/lib/shared-config.ts — new shared config infrastructure at ~/.studio/shared.json for data shared between Desktop and CLI (auth token + locale)
  • Wire Desktop auth reads/writes to shared-config instead of appdata
  • Wire Desktop locale reads/writes to shared-config instead of appdata
  • Wire CLI auth reads/writes to shared-config instead of appdata
  • Move CLI-only AI settings (aiProvider, anthropicApiKey) from appdata to cli.json
  • Move CLI telemetry (lastBumpStats) from appdata to cli.json
  • Remove authToken, locale, AI fields, and lastBumpStats from appdata types and schemas
  • Add 20 tests for shared-config and update all affected test files

After these changes the write boundaries are:

  • Desktopappdata-v1.json (sites, snapshots, preferences) + shared.json (auth, locale)
  • CLIcli.json (sites, AI settings, telemetry) + shared.json (auth) + appdata-v1.json (snapshots only)

Testing Instructions

  • Run npm run typecheck — should pass with no errors
  • Run npm test — all 1292 tests should pass
  • Manual: launch Desktop app, log in via WordPress.com — verify ~/.studio/shared.json contains the auth token
  • Manual: change language in Preferences — verify ~/.studio/shared.json contains the locale
  • Manual: run studio auth login from CLI — verify shared.json is updated
  • Manual: run studio ai and switch provider/enter API key — verify ~/.studio/cli.json contains aiProvider and anthropicApiKey

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

Move auth token reads/writes from appdata to shared-config for Desktop+CLI. Move locale reads/writes to shared-config (Desktop saves, CLI reads). Move aiProvider and anthropicApiKey from shared-config to cli-config (CLI-only). Remove auth and locale fields from appdata schema and types since they're now in separate files. Update all imports and tests accordingly.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@bcotrim bcotrim self-assigned this Mar 16, 2026
@bcotrim bcotrim marked this pull request as ready for review March 17, 2026 18:24
@bcotrim bcotrim requested review from a team and fredrikekelund March 17, 2026 18:25
Copy link
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments. Shared config error handling and using _events instead of a shared config file watcher are the big things we need to address, IMO.

Haven't tested yet but will do that now 👍

Comment on lines +59 to +62
if ( error instanceof z.ZodError || error instanceof SyntaxError ) {
return structuredClone( DEFAULT_SHARED_CONFIG );
}
throw new Error( 'Failed to read shared config file.' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in apps/cli/lib/cli-config/core.ts, we should look for differing version props here and say something like "A newer version of Studio or the Studio CLI is installed on your system" if the version differs.

Comment on lines +118 to +120
} catch {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relates to the error handling I suggested in readSharedConfig. I'm not sure what the best approach is here…

If the user is running an old version of the CLI but have a newer version of Studio installed (that uses a new schema for the shared config file), then we should prompt them to upgrade.

However, we still need both programs to recover from errors well enough to at least report the right thing to the user. This is especially true for Studio, which needs to be able to auto-update as usual.


const sharedConfigSchema = z
.object( {
version: z.number().default( 1 ),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put the version in a constant and add a comment explaining that any schema update must maintain backwards compatibility. If the change is breaking, the version should be updated and a data migration function should be added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The important thing to test here, IMO, is how we recover from schema-related errors. We have some of that in the readSharedConfig tests, but I think we can expand on that (along with updating the logic in the actual source code)

await unlockFileAsync( LOCKFILE_PATH );
}

export async function updateCliConfig(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export async function updateCliConfig(
export async function updateCliConfigWithPartial(

Just a suggestion

}
}

export async function readAuthToken(): Promise< StoredToken | null > {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exporting functions that return just parts of the config is clearly great for reducing the amount of test mocks. Nice 👍

displayName: z.string().default( '' ),
} );

export type StoredToken = z.infer< typeof authTokenSchema >;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type StoredToken = z.infer< typeof authTokenSchema >;
export type StoredAuthToken = z.infer< typeof authTokenSchema >;

I'd include "auth" in the name somehow

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouff… I didn't consider the HTTPS certificates. We should put these in ~/.studio, too, IMO. Let's do that in a separate PR, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created STU-1416 to track this

Comment on lines +71 to +85
function getAppdataDirectory(): string {
if ( process.env.E2E && process.env.E2E_APP_DATA_PATH ) {
return path.join( process.env.E2E_APP_DATA_PATH, 'Studio' );
}

if ( process.platform === 'win32' ) {
if ( ! process.env.APPDATA ) {
throw new LoggerError( __( 'Studio config file path not found.' ) );
}

return path.join( process.env.APPDATA, 'Studio' );
}

return path.join( os.homedir(), 'Library', 'Application Support', 'Studio' );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use getAppdataDirectory from apps/cli/lib/server-files.ts here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use _events for listening to auth changes, too, instead of watching the shared config file?

Copy link
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works as expected in testing 👍 Error handling for the shared config is really key to get right. I'll still approve now to keep up momentum, since we aren't landing straight to trunk

Comment on lines +83 to +90
if ( ! token ) {
setIsAuthenticated( false );
setClient( undefined );
setWpcomClient( undefined );
setUser( undefined );
return;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth logout command didn't trigger a change in authentication status in the app for me when I tested it the first time. Probably because the shared config file didn't exist when I started Studio. I guess this would be solved by moving to an _events architecture for auth events instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update this file to import aiProviderSchema from apps/cli/lib/cli-config/core.ts. We should have a single definition for that

Comment on lines +159 to +162
isReady: async () => {
const { anthropicApiKey } = await readCliConfig();
return Boolean( anthropicApiKey );
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to remember to ping the Architecture team before landing this to trunk, to let them know that they need to manually migrate AI-related props from the appdata file to the CLI config file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created STU-1416 to track this

Comment on lines +22 to 25
vi.mock( '@studio/common/lib/shared-config', async ( importOriginal ) => ( {
...( await importOriginal< typeof import('@studio/common/lib/shared-config') >() ),
readAuthToken: vi.fn(),
} ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
vi.mock( '@studio/common/lib/shared-config', async ( importOriginal ) => ( {
...( await importOriginal< typeof import('@studio/common/lib/shared-config') >() ),
readAuthToken: vi.fn(),
} ) );
vi.mock( import( '@studio/common/lib/shared-config' ), async ( importOriginal ) => ( {
...( await importOriginal() ),
readAuthToken: vi.fn(),
} ) );

Tip: I stumbled upon this recently, that the first argument to vi.mock can be an import() call, which makes Vitest aware of the module types.

export type AiProviderId = keyof typeof AI_PROVIDERS;

export const aiProviderSchema = z.enum( [ 'wpcom', 'anthropic-claude', 'anthropic-api-key' ] );
export { aiProviderSchema };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to re-export this definition?


const sharedConfigSchema = z
.object( {
version: z.number().default( SHARED_CONFIG_VERSION ),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
version: z.number().default( SHARED_CONFIG_VERSION ),
version: z.literal( SHARED_CONFIG_VERSION ),

I should have called this out in apps/cli/lib/cli-config/core.ts, too, but this is an important distinction: I think the zod schema parsing should fail unless the version field matches what we expect.

If we don't do this, then the schema parsing might fail for other reasons, but it's not as easy for us to see why it's happening.

We can't just change this line to z.literal instead, we also need a base schema containing just the version field to try with. I'm basically imagining the logic we have in apps/cli/lib/cli-config/core.ts, just with z.literal instead of z.number().default()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants